﻿using System;
using System.Collections.Generic;
using System.Text;
using RoughJs.Ast;

namespace RoughJs.E4X
{
    /// <summary>
    /// 2010-01-5 ----- 2010-01-29
    /// 陈曦 1.0
    /// Microsoft Public License (Ms-PL)
    ///This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.
    ///1. Definitions
    ///The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
    ///A "contribution" is the original software, or any additions or changes to the software.
    ///A "contributor" is any person that distributes its contribution under this license.
    ///"Licensed patents" are a contributor's patent claims that read directly on its contribution.
    ///2. Grant of Rights
    ///(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
    ///(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
    ///3. Conditions and Limitations
    ///(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
    ///(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
    ///(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
    ///(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
    ///(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. 
    /// </summary>
    public class XMLList : XMLObjectImpl, Function
    {
        private XmlNode.InternalList _annos;
        private XMLObjectImpl targetObject = null;
        private XmlNode.QName targetProperty = null;

        public XMLList(XMLLibImpl lib, Scriptable scope, XMLObject prototype)
            : base(lib, scope, prototype)
        {

            _annos = new XmlNode.InternalList();
        }

        /* TODO Will probably end up unnecessary as we move things around */
        public XmlNode.InternalList getNodeList()
        {
            return _annos;
        }

        public XML item(int index)
        {
            return _annos != null
                ? getXmlFromAnnotation(index) : createEmptyXML();
        }

        private XML getXmlFromAnnotation(int index)
        {
            return getXML(_annos, index);
        }
        //private XML getXML(XmlNode.InternalList _annos, int index)
        //{
        //    if (index >= 0 && index < length())
        //    {
        //        return xmlFromNode(_annos.item(index));
        //    }
        //    else
        //    {
        //        return null;
        //    }
        //}

        public XML createEmptyXML()
        {
            return newXML(XmlNode.createEmpty(getProcessor()));
        }
        public void addToList(Object toAdd)
        {
            _annos.addToList(toAdd);
        }
        public void setTargets(XMLObjectImpl object1, XmlNode.QName property)
        {
            targetObject = object1;
            targetProperty = property;
        }


        public void replace(int index, XML xml)
        {
            if (index < length())
            {
                XmlNode.InternalList newAnnoList = new XmlNode.InternalList();
                newAnnoList.add(_annos, 0, index);
                newAnnoList.add(xml);
                newAnnoList.add(_annos, index + 1, length());
                _annos = newAnnoList;
            }
        }

        private void insert(int index, XML xml)
        {
            if (index < length())
            {
                XmlNode.InternalList newAnnoList = new XmlNode.InternalList();
                newAnnoList.add(_annos, 0, index);
                newAnnoList.add(xml);
                newAnnoList.add(_annos, index, length());
                _annos = newAnnoList;
            }
        }

        //
        //
        //  methods overriding ScriptableObject
        //
        //


        public override String getClassName()
        {
            return "XMLList";
        }

        //
        //
        //  methods overriding IdScriptableObject
        //
        //


        public override Object get(int index, Scriptable start)
        {
            //Log("get index: " + index);

            if (index >= 0 && index < length())
            {
                return getXmlFromAnnotation(index);
            }
            else
            {
                return ScriptableObject.NOT_FOUND;
            }
        }


        public override bool hasXMLProperty(XMLName xmlName)
        {
            bool result = false;

            // Has now should return true if the property would have results > 0 or
            // if it's a method name
            String name = xmlName.localName();
            if ((getPropertyList(xmlName).length() > 0) ||
                (getMethod(name) != NOT_FOUND))
            {
                result = true;
            }

            return result;
        }


        public override bool has(int index, Scriptable start)
        {
            return 0 <= index && index < length();
        }


        public override void putXMLProperty(XMLName xmlName, Object value)
        {
            //Log("put property: " + name);

            // Special-case checks for undefined and null
            if (value == null)
            {
                value = "null";
            }
            else if (value is Undefined)
            {
                value = "undefined";
            }

            if (length() > 1)
            {
                throw ScriptRuntime.typeError(
                   "Assignment to lists with more than one item is not supported");
            }
            else if (length() == 0)
            {
                // Secret sauce for super-expandos.
                // We set an element here, and then add ourselves to our target.
                if (targetObject != null && targetProperty != null &&
                    targetProperty.getLocalName() != null)
                {
                    // Add an empty element with our targetProperty name and 
                    // then set it.
                    XML xmlValue = newTextElementXML(null, targetProperty, null);
                    addToList(xmlValue);

                    if (xmlName.isAttributeName())
                    {
                        setAttribute(xmlName, value);
                    }
                    else
                    {
                        XML xml = item(0);
                        xml.putXMLProperty(xmlName, value);

                        // Update the list with the new item at location 0.
                        replace(0, item(0));
                    }

                    // Now add us to our parent
                    XMLName name2 = XMLName.formProperty(
                            targetProperty.getNamespace().getUri(),
                            targetProperty.getLocalName());
                    targetObject.putXMLProperty(name2, this);
                }
                else
                {
                    throw ScriptRuntime.typeError(
                      "Assignment to empty XMLList without targets not supported");
                }
            }
            else if (xmlName.isAttributeName())
            {
                setAttribute(xmlName, value);
            }
            else
            {
                XML xml = item(0);
                xml.putXMLProperty(xmlName, value);

                // Update the list with the new item at location 0.
                replace(0, item(0));
            }
        }


        public override Object getXMLProperty(XMLName name)
        {
            return getPropertyList(name);
        }

        private void replaceNode(XML xml, XML with)
        {
            xml.replaceWith(with);
        }


        public override void put(int index, Scriptable start, Object value)
        {
            Object parent1 = Undefined.instance;
            // Convert text into XML if needed.
            XMLObject xmlValue;

            // Special-case checks for undefined and null
            if (value == null)
            {
                value = "null";
            }
            else if (value is Undefined)
            {
                value = "undefined";
            }

            if (value is XMLObject)
            {
                xmlValue = (XMLObject)value;
            }
            else
            {
                if (targetProperty == null)
                {
                    xmlValue = newXMLFromJs(value.ToString());
                }
                else
                {
                    //    Note that later in the code, we will use this as an argument to replace(int,value)
                    //    So we will be "replacing" this element with itself
                    //    There may well be a better way to do this
                    //    TODO    Find a way to refactor this whole method and simplify it
                    xmlValue = item(index);
                    ((XML)xmlValue).setChildren(value);
                }
            }

            // Find the parent
            if (index < length())
            {
                parent1 = item(index).parent();
            }
            else
            {
                // Appending
                parent1 = parent();
            }

            if (parent1 is XML)
            {
                // found parent, alter doc
                XML xmlParent = (XML)parent1;

                if (index < length())
                {
                    // We're replacing the the node.
                    XML xmlNode = getXmlFromAnnotation(index);

                    if (xmlValue is XML)
                    {
                        replaceNode(xmlNode, (XML)xmlValue);
                        replace(index, xmlNode);
                    }
                    else if (xmlValue is XMLList)
                    {
                        // Replace the first one, and add the rest on the list.
                        XMLList list = (XMLList)xmlValue;

                        if (list.length() > 0)
                        {
                            int lastIndexAdded = xmlNode.childIndex();
                            replaceNode(xmlNode, list.item(0));
                            replace(index, list.item(0));

                            for (int i = 1; i < list.length(); i++)
                            {
                                xmlParent.insertChildAfter(xmlParent.getXmlChild(lastIndexAdded), list.item(i));
                                lastIndexAdded++;
                                insert(index + i, list.item(i));
                            }
                        }
                    }
                }
                else
                {
                    // Appending
                    xmlParent.appendChild(xmlValue);
                    addToList(xmlParent.getXmlChild(index));
                }
            }
            else
            {
                // Don't all have same parent, no underlying doc to alter
                if (index < length())
                {
                    XML xmlNode = getXML(_annos, index);

                    if (xmlValue is XML)
                    {
                        replaceNode(xmlNode, (XML)xmlValue);
                        replace(index, xmlNode);
                    }
                    else if (xmlValue is XMLList)
                    {
                        // Replace the first one, and add the rest on the list.
                        XMLList list = (XMLList)xmlValue;

                        if (list.length() > 0)
                        {
                            replaceNode(xmlNode, list.item(0));
                            replace(index, list.item(0));

                            for (int i = 1; i < list.length(); i++)
                            {
                                insert(index + i, list.item(i));
                            }
                        }
                    }
                }
                else
                {
                    addToList(xmlValue);
                }
            }
        }

        private XML getXML(XmlNode.InternalList _annos, int index)
        {
            if (index >= 0 && index < length())
            {
                return xmlFromNode(_annos.item(index));
            }
            else
            {
                return null;
            }
        }


        public override void deleteXMLProperty(XMLName name)
        {
            for (int i = 0; i < length(); i++)
            {
                XML xml = getXmlFromAnnotation(i);

                if (xml.isElement())
                {
                    xml.deleteXMLProperty(name);
                }
            }
        }


        public override void delete(int index)
        {
            if (index >= 0 && index < length())
            {
                XML xml = getXmlFromAnnotation(index);

                xml.remove();

                internalRemoveFromList(index);
            }
        }


        public override Object[] getIds()
        {
            Object[] enumObjs;

            if (isPrototype())
            {
                enumObjs = new Object[0];
            }
            else
            {
                enumObjs = new Object[length()];

                for (int i = 0; i < enumObjs.Length; i++)
                {
                    enumObjs[i] = i;
                }
            }

            return enumObjs;
        }

        public Object[] getIdsForDebug()
        {
            return getIds();
        }


        // XMLList will remove will delete all items in the list (a set delete) this differs from the XMLList delete operator.
        public void remove()
        {
            int nLen = length();
            for (int i = nLen - 1; i >= 0; i--)
            {
                XML xml = getXmlFromAnnotation(i);
                if (xml != null)
                {
                    xml.remove();
                    internalRemoveFromList(i);
                }
            }
        }

        //XML item(int index) {
        //    return _annos != null
        //        ? getXmlFromAnnotation(index) : createEmptyXML();
        //}

        private void setAttribute(XMLName xmlName, Object value)
        {
            for (int i = 0; i < length(); i++)
            {
                XML xml = getXmlFromAnnotation(i);
                xml.setAttribute(xmlName, value);
            }
        }
        private void internalRemoveFromList(int index)
        {
            _annos.remove(index);
        }

        //public void addToList(Object toAdd) {
        //     _annos.addToList(toAdd);
        // }

        //
        //
        // Methods from section 12.4.4 in the spec
        //
        //


        public override XMLList child(int index)
        {
            XMLList result = newXMLList();

            for (int i = 0; i < length(); i++)
            {
                result.addToList(getXmlFromAnnotation(i).child(index));
            }

            return result;
        }


        public override XMLList child(XMLName xmlName)
        {
            XMLList result = newXMLList();

            for (int i = 0; i < length(); i++)
            {
                result.addToList(getXmlFromAnnotation(i).child(xmlName));
            }

            return result;
        }


        public override void addMatches(XMLList rv, XMLName name)
        {
            for (int i = 0; i < length(); i++)
            {
                getXmlFromAnnotation(i).addMatches(rv, name);
            }
        }


        public override XMLList children()
        {
            List<XML> list = new List<XML>();

            for (int i = 0; i < length(); i++)
            {
                XML xml = getXmlFromAnnotation(i);

                if (xml != null)
                {
                    Object o = xml.children();
                    if (o is XMLList)
                    {
                        XMLList childList = (XMLList)o;

                        int cChildren = childList.length();
                        for (int j = 0; j < cChildren; j++)
                        {
                            list.Add(childList.item(j));
                        }
                    }
                }
            }

            XMLList allChildren = newXMLList();
            int sz = list.Count;

            for (int i = 0; i < sz; i++)
            {
                allChildren.addToList(list[i]);
            }

            return allChildren;
        }


        public override XMLList comments()
        {
            XMLList result = newXMLList();

            for (int i = 0; i < length(); i++)
            {
                XML xml = getXmlFromAnnotation(i);
                result.addToList(xml.comments());
            }

            return result;
        }


        public override XMLList elements(XMLName name)
        {
            XMLList rv = newXMLList();
            for (int i = 0; i < length(); i++)
            {
                XML xml = getXmlFromAnnotation(i);
                rv.addToList(xml.elements(name));
            }
            return rv;
        }


        public override bool contains(Object xml)
        {
            bool result = false;

            for (int i = 0; i < length(); i++)
            {
                XML member = getXmlFromAnnotation(i);

                if (member.equivalentXml(xml))
                {
                    result = true;
                    break;
                }
            }

            return result;
        }


        public override XMLObjectImpl copy()
        {
            XMLList result = newXMLList();

            for (int i = 0; i < length(); i++)
            {
                XML xml = getXmlFromAnnotation(i);
                result.addToList(xml.copy());
            }

            return result;
        }


        public override bool hasOwnProperty(XMLName xmlName)
        {
            if (isPrototype())
            {
                String property = xmlName.localName();
                return (findPrototypeId(property) != 0);
            }
            else
            {
                return (getPropertyList(xmlName).length() > 0);
            }
        }


        public override bool hasComplexContent()
        {
            bool complexContent;
            int length1 = length();

            if (length1 == 0)
            {
                complexContent = false;
            }
            else if (length1 == 1)
            {
                complexContent = getXmlFromAnnotation(0).hasComplexContent();
            }
            else
            {
                complexContent = false;

                for (int i = 0; i < length1; i++)
                {
                    XML nextElement = getXmlFromAnnotation(i);
                    if (nextElement.isElement())
                    {
                        complexContent = true;
                        break;
                    }
                }
            }

            return complexContent;
        }


        public override bool hasSimpleContent()
        {
            if (length() == 0)
            {
                return true;
            }
            else if (length() == 1)
            {
                return getXmlFromAnnotation(0).hasSimpleContent();
            }
            else
            {
                for (int i = 0; i < length(); i++)
                {
                    XML nextElement = getXmlFromAnnotation(i);
                    if (nextElement.isElement())
                    {
                        return false;
                    }
                }
                return true;
            }
        }


        public override int length()
        {
            int result = 0;

            if (_annos != null)
            {
                result = _annos.length();
            }

            return result;
        }


        public override void normalize()
        {
            for (int i = 0; i < length(); i++)
            {
                getXmlFromAnnotation(i).normalize();
            }
        }



        public override Object parent()
        {
            if (length() == 0) return Undefined.instance;

            XML candidateParent = null;

            for (int i = 0; i < length(); i++)
            {
                Object currParent = getXmlFromAnnotation(i).parent();
                if (!(currParent is XML)) return Undefined.instance;
                XML xml = (XML)currParent;
                if (i == 0)
                {
                    // Set the first for the rest to compare to.
                    candidateParent = xml;
                }
                else
                {
                    if (candidateParent.is1(xml))
                    {
                        //    keep looking
                    }
                    else
                    {
                        return Undefined.instance;
                    }
                }
            }
            return candidateParent;
        }


        public override XMLList processingInstructions(XMLName xmlName)
        {
            XMLList result = newXMLList();

            for (int i = 0; i < length(); i++)
            {
                XML xml = getXmlFromAnnotation(i);

                result.addToList(xml.processingInstructions(xmlName));
            }

            return result;
        }


        public override bool propertyIsEnumerable(Object name)
        {
            long index;
            if (name is int)
            {
                index = Convert.ToInt32(name);
            }
            else if (ScriptRuntime.isNumber(name))
            {
                double x = ScriptRuntime.toNumber(name);
                index = (long)x;
                if (index != x)
                {
                    return false;
                }
                if (index == 0 && 1.0 / x < 0)
                {
                    // Negative 0
                    return false;
                }
            }
            else
            {
                String s = ScriptRuntime.toString(name);
                index = ScriptRuntime.testUint32String(s);
            }
            return (0 <= index && index < length());
        }

        public override XMLList text()
        {
            XMLList result = newXMLList();

            for (int i = 0; i < length(); i++)
            {
                result.addToList(getXmlFromAnnotation(i).text());
            }

            return result;
        }


        public override String toString()
        {
            //    ECMA357 10.1.2
            if (hasSimpleContent())
            {
                StringBuilder sb = new StringBuilder();

                for (int i = 0; i < length(); i++)
                {
                    XML next = getXmlFromAnnotation(i);
                    if (next.isComment() || next.isProcessingInstruction())
                    {
                        //    do nothing
                    }
                    else
                    {
                        String str = next.toString();
                        sb.Append(str);
                    }
                }

                return sb.ToString();
            }
            else
            {
                return toXMLString();
            }
        }


        public override String toSource(int indent)
        {
            return toXMLString();
        }


        public override String toXMLString()
        {
            //    See ECMA 10.2.1
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < length(); i++)
            {
                if (getProcessor().isPrettyPrinting() && i != 0)
                {
                    sb.Append('\n');
                }
                sb.Append(getXmlFromAnnotation(i).toXMLString());
            }
            return sb.ToString();
        }


        public override Object valueOf()
        {
            return this;
        }

        public override bool equivalentXml(Object target)
        {
            bool result = false;

            // Zero length list should equate to undefined
            if (target is Undefined && length() == 0)
            {
                result = true;
            }
            else if (length() == 1)
            {
                result = getXmlFromAnnotation(0).equivalentXml(target);
            }
            else if (target is XMLList)
            {
                XMLList otherList = (XMLList)target;

                if (otherList.length() == length())
                {
                    result = true;

                    for (int i = 0; i < length(); i++)
                    {
                        if (!getXmlFromAnnotation(i).equivalentXml(otherList.getXmlFromAnnotation(i)))
                        {
                            result = false;
                            break;
                        }
                    }
                }
            }

            return result;
        }

        private XMLList getPropertyList(XMLName name)
        {
            XMLList propertyList = newXMLList();
            XmlNode.QName qname = null;

            if (!name.isDescendants() && !name.isAttributeName())
            {
                // Only set the targetProperty if this is a regular child get
                // and not a descendant or attribute get
                qname = name.toQname();
            }

            propertyList.setTargets(this, qname);

            for (int i = 0; i < length(); i++)
            {
                propertyList.addToList(
                    getXmlFromAnnotation(i).getPropertyList(name));
            }

            return propertyList;
        }

        private Object applyOrCall(bool isApply,
            Context cx, Scriptable scope,
            Scriptable thisObj, Object[] args)
        {
            String methodName = isApply ? "apply" : "call";
            if (!(thisObj is XMLList) ||
                ((XMLList)thisObj).targetProperty == null)
                throw ScriptRuntime.typeError1("msg.isnt.function",
                    methodName);

            return ScriptRuntime.applyOrCall(isApply, cx, scope, thisObj, args);
        }


        public override Object jsConstructor(Context cx, bool inNewExpr,
                                       Object[] args)
        {
            if (args.Length == 0)
            {
                return newXMLList();
            }
            else
            {
                Object arg0 = args[0];
                if (!inNewExpr && arg0 is XMLList)
                {
                    // XMLList(XMLList) returns the same object.
                    return arg0;
                }
                return newXMLListFrom(arg0);
            }
        }

        /**
         * See ECMA 357, 11_2_2_1, Semantics, 3_e.
         */

        public override Scriptable getExtraMethodSource(Context cx)
        {
            if (length() == 1)
            {
                return getXmlFromAnnotation(0);
            }
            return null;
        }

        public Object call(Context cx, Scriptable scope, Scriptable thisObj,
            Object[] args)
        {
            // This XMLList is being called as a Function.
            // Let's find the real Function object.
            if (targetProperty == null)
                throw ScriptRuntime.notFunctionError(this);

            String methodName = targetProperty.getLocalName();

            bool isApply = (methodName == "apply");
            if (isApply || (methodName == "call"))
                return applyOrCall(isApply, cx, scope, thisObj, args);

            Callable method = ScriptRuntime.getElemFunctionAndThis(
                this, methodName, cx);
            // Call lastStoredScriptable to clear stored thisObj
            // but ignore the result as the method should use the supplied
            // thisObj, not one from redirected call
            ScriptRuntime.lastStoredScriptable(cx);
            return method.call(cx, scope, thisObj, args);
        }

        public Scriptable construct(Context cx, Scriptable scope, Object[] args)
        {
            throw ScriptRuntime.typeError1("msg.not.ctor", "XMLList");
        }
        public override XML getXML()
        {
            if (length() == 1) return getXmlFromAnnotation(0);
            return null;
        }
    }// end for class
}
